chore(repo): consolidate tooling configuration into .config/ - #53
Merged
Conversation
The root mixed normative content, project documents and tool configuration, and nothing distinguished the files that must be at the root — Git, EditorConfig, Task, none of which accepts a config-path flag — from the ones that were merely there by default. Every new linter arrived at the root and the set only grew. Adopt the convention musher-dev/development-container and musher-dev/platform already share: bucket by concern under .config/, no leading dot on filenames, and every caller names its config with the tool's own flag. lefthook.yml stays at .config/'s top level because lefthook's config search does not descend past .config/lefthook.* — bucketing it would stop every hook running, silently. Enforce it rather than document it. Platform shipped the prose version of this rule with no gate, and in that state two non-configs accreted inside the directory and four lint tools ran on defaults. tools/src/config.ts implements CFG-01..CFG-08 with the same codes the sibling repositories report, so the three share a vocabulary for reporting a breach even though this one runs the check from Bun rather than Python. Moving cspell surfaced the hazard the rule is about. cspell resolves ignorePaths against globRoot, which defaults to the config file's directory, so the move re-rooted every repo-relative ignore against .config/spelling/ where none of them matched — the check kept passing while covering strictly less. globRoot is now set explicitly. Verified by file count, not by a red build: a check that has stopped enforcing anything does not fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
Two more tool configs into .config/, both passed by explicit path. actionlint ran on defaults. Its config now declares an empty self-hosted-runner label list and an empty config-variables list, which turns `vars.TYPO` from an empty string that silently changes what a job does into a build failure -- verified by planting an undeclared variable and watching it fail. markdownlint is new. Five relaxations, each with its reason in the file: MD033 allows `a` and `br` because the spec.md anchors are load-bearing (conformance fixtures link to them) and `br` is the only in-cell line break; MD036 because a bold lead-in is this repository's house style in every ADR; MD060 because it is presentation, has no autofix, and adopting it would mean hand-realigning 263 table pipes across three normative spec.md files for no rendering difference. pull_request_template.md is excluded from the glob: it is a form fragment GitHub renders into a textarea, so MD041 would be asking it to grow a title. The rest of the backlog was small enough to fix outright, and markdownlint made both edits itself: two bare e-mail addresses are now autolinked, and three double blank lines in spec.md are single. No prose changed. Both flags verified live against a path that does not exist -- exit 3 and 2 respectively, not a silent fallback to defaults. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
One 330-line file held setup, build, eighteen checks, the CI entry points and
the ledger writes. It now holds the entry points and includes the rest, which
is the shape musher-dev/development-container and musher-dev/platform already
use.
Every pre-existing task name is unchanged -- CI, the hooks and CONTRIBUTING all
call them by name. `task --list` before and after differs only by the three new
tasks. Two mechanics make that hold, and both were traps worth recording in the
files themselves:
- A relative `dir:` in an included taskfile resolves against that file's own
directory, so `dir: tools` would have silently meant `taskfiles/tools`. Every
moved task uses `{{.ROOT_DIR}}`.
- A bare `task: check:format` inside an included file resolves within that
file's namespace, as `ci:check:format`, and fails. The `ci` module anchors
each reference to the root with a leading colon.
`check` and `setup` stay in the root rather than becoming a module `default`.
An included `default` is reachable as the bare namespace, but `task --list`
renders it `check:default`, which hides the command a newcomer most needs.
Also documents the new layout: CLAUDE.md gains .config/ and taskfiles/ in the
layout block and a non-negotiable for where tool config goes, and CONTRIBUTING
gains check:config and check:md in the table of what `task check` runs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
CONTRIBUTING.md, CODE_OF_CONDUCT.md and SECURITY.md are the three GitHub resolves from .github/ as readily as from the root, and moving them leaves the root holding the contract and the documents that describe it. Every link that pointed at them moved in the same change; check:links is what proves it. Three stay at the root, and not by omission: - LICENSE and NOTICE, because .github/ is not a supported location for either. GitHub's license detection reads the root only, so moving LICENSE would drop the licence from the repository page and the API, and Apache-2.0 expects NOTICE to travel with the work. - GOVERNANCE.md, which GitHub *does* support in .github/, because a normative spec.md links to it and so does every ADR. It is contract surface, and ADR 0011's own test puts it at the root: what is visible there is content you read and edit, what is dotted is machinery that operates on it. Renumbers the tooling-configuration ADR from 0007 to 0011. 0007 was already taken by the naming-conventions ADR on the commit this branch started from. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
.editorconfig and devcontainer.json each stated whitespace, line endings and encoding independently, and they disagreed. .editorconfig exempts Markdown from trailing-whitespace trimming because two trailing spaces are a hard line break; devcontainer.json trimmed unconditionally. In-container VS Code was quietly reformatting prose against the rule the repository had written down. musher-dev/development-container resolves this the other way -- it deletes .editorconfig and keeps devcontainer.json -- and its own rationale names the exception this repository falls under: those settings reach VS Code inside the container and nothing else, so a project with contributors working outside it should keep .editorconfig. CONTRIBUTING documents that path, and anyone may send a patch to a public specification. So .editorconfig keeps the whitespace rules and gains the reason for the Markdown exemption, devcontainer.json drops the four duplicated keys, and the EditorConfig extension is added so in-container VS Code actually reads the file. devcontainer.json keeps what is genuinely VS Code's own: rulers, format-on-save, formatter bindings, the local schema associations. Recorded as ADR 0011 §6. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
7 tasks
justinmerrell
added a commit
that referenced
this pull request
Aug 25, 2026
The main-branch ruleset required one approving review on every pull request, and CODEOWNERS opened with a `*` catch-all naming the only maintainer. Since GitHub cannot request a review from a pull request's author, that combination blocked every pull request the maintainer opened, and every one was merged by spending the OrganizationAdmin bypass — #42, #45, #47, #48, #52, #53, #55 and #57 all carry `reviewDecision: REVIEW_REQUIRED`. A control satisfied on no pull request and stepped over on all of them enforced nothing, while still charging every contributor the wait. Adopt the mechanism musher-dev/platform already uses, so that moving between the two repositories does not mean relearning when review is required: pair `required_approving_review_count: 0` with `require_code_owner_review: true`, and reduce CODEOWNERS to the two paths that define the gate itself. A pull request touching no owned path now merges on green CI; one touching `.github/CODEOWNERS` or `.github/rulesets/` still needs its owner. Both halves break silently — a catch-all restores the blanket gate from one side, a count of 1 from the other — so `tools/src/rulesets.ts` (RUL-01..RUL-09) keeps them in step from inside the existing Lint job. RUL-09 has no counterpart upstream: it rejects a required status check that no workflow publishes, or that a `paths:`-filtered workflow publishes, which are the two ways a required context hangs a pull request forever. It is also why the check runs inside Lint rather than as a workflow of its own. Reconcile two drifts the committed JSON had accumulated since 2026-08-08, both in the file's favour: `Signed off` is now genuinely a required check rather than only a declared one, and release-tags no longer grants the OrganizationAdmin bypass its own documentation says it does not have. GitHub waives the code-owner requirement for a pull request's author, which with a single owner is the whole gate on the paths that matter most. codeowners-notice.yml posts a sticky comment on self-owned edits so the waiver is visible; it is a notifier and must never become a required check. GOVERNANCE.md advertised approval as mechanical in three places and CONTRIBUTING.md in one; they now say plainly which obligations are enforced and which are asked for. ADR 0015 records the trade, including that owning specifications/ becomes worth revisiting the moment there is a second maintainer. Signed-off-by: Justin Merrell <merrelljustin@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changes
Consolidates every tool configuration into
.config/, adopting the conventionmusher-dev/development-containerandmusher-dev/platformalready share, andmakes it a build failure rather than a comment. Along the way: two new linters,
the
Taskfile.ymlsplit into modules, and the community health files moved into.github/.Nothing under
specifications/orconformance/changes behaviour, and nocommit is scoped to a family — this releases nothing.
The layout
Root keeps only what can be nowhere else:
Taskfile.yml,.gitignore,.gitattributes,.editorconfig,LICENSE,NOTICE,README.md,GOVERNANCE.md, and the two published data artifacts.tools/biome.jsonandtools/tsconfig.jsonstay with their package.The gate
tools/src/config.ts(task check:config) enforces CFG-01..CFG-08 with thesame codes the sibling repositories report from their Python
repo config check— three repositories, one convention, one vocabulary.config.test.tsprovokes every code against a throwaway tree, because a gate that cannot fail is
indistinguishable from no gate.
Two silent failures this found
cspell's
globRoot. cspell resolvesignorePathsagainst the config file'sown directory, so moving it two levels down re-rooted every repo-relative ignore
against
.config/spelling/, where none matched. The check kept passing whilecovering strictly less. Caught by comparing file counts (53 before, 53 after),
not by a red build — a check that has stopped enforcing anything does not fail.
globRootis now explicit..editorconfigvsdevcontainer.json. Both stated whitespace independentlyand disagreed:
.editorconfigexempts Markdown from trailing-whitespacetrimming because two trailing spaces are a hard line break,
devcontainer.jsontrimmed unconditionally. In-container VS Code was reformatting prose against the
repository's own rule.
.editorconfigis now the single source.New linters
config-variables: []turnsvars.TYPOfrom anempty string that silently changes a job into a build failure — verified by
planting one.
tools/. Five relaxations, each with itsreason in the file.
MD060is off: it has no autofix, and adopting it meanthand-realigning 263 table pipes across three normative
spec.mdfiles for norendering difference. The rest of the backlog was fixed — two bare e-mail
addresses autolinked, three double blank lines closed.
Every new flag was verified live against a path that does not exist, so a
silently-ignored
--configcannot hide behind a green build.Taskfile split
Every pre-existing task name is unchanged;
task --listdiffers only bycheck:config,check:mdandfmt:md. Two traps are recorded in the files: arelative
dir:in an included taskfile resolves against that file'sdirectory, and a bare
task: check:formatinside an included file resolveswithin its own namespace.
Community health files
CONTRIBUTING.md,CODE_OF_CONDUCT.mdandSECURITY.mdmoved to.github/.LICENSEandNOTICEcannot — GitHub detects a licence at the root only.GOVERNANCE.mdcould, but a normativespec.mdlinks to it and so does everyADR: it is contract surface, not a community file.
Why
Root dotfiles accumulate because every tool's README says to put them there, and
nothing distinguishes root-by-necessity (Git, EditorConfig, Task) from
root-by-default (lefthook, cspell). Platform shipped the prose version of this
rule with no gate and watched two non-configs accrete inside the directory and
four lint tools run on defaults. Rationale in
ADR 0011.
Verification
task checkgreen locally, including all three new checks.task --listdiffed before and after the split: only the three new tasks.CFG-0Nprovoked deliberately and confirmed to fire, then reverted.rm -rf .git/hooks && task setup:hooks) andobserved running from
.config/lefthook.yml, including the newconfigjob.git statusclean underconformance/; the two byte-sensitive-textparserfixtures untouched.
Known gap
YAML linting is not in this PR. yamllint is Python-only and this dev
container deliberately ships no Python or uv — its header says so. The agreed
alternative,
google/yamlfmtin lint mode, could not be installed either: thisenvironment truncates large release downloads from GitHub's asset CDN, so it
could not be run or its output evaluated even once. Adding a check whose first
real execution is in CI would break the rule this PR just wrote down. Worth a
follow-up from a machine that can run it.